/// [ViewportScroller]：用于计算光标在视口中的可见性，并决定是否需要滚动。
///
/// 功能：
/// - 保证光标（按行计算）在视口范围内可见；
/// - 不直接操作滚动，而是返回目标滚动值，由宿主决定何时调用 `setState` 或更新滚动位置；
/// - 避免光标被遮挡或超出可见区域。
///
/// 使用流程示例：
/// 1. 在光标位置变化时调用 [computeScrollToReveal]；
/// 2. 如果返回非空值，则更新 `scrollY`；
/// 3. 如果返回 `null`，说明光标已在视口内，不需要滚动。
class ViewportScroller {
  const ViewportScroller();

  /// 计算光标是否需要滚动以保证可见。
  ///
  /// - [cursorPosition] 文档中的光标位置（绝对偏移，范围 `0..text.length`）
  /// - [lines] 文本的按行拆分结果（需与实际渲染一致）
  /// - [viewportHeight] 当前视口高度
  /// - [currentScrollY] 当前滚动的偏移值
  /// - [padding] 视口顶部和底部的额外内边距（用于让行顶/行底稍微露出）
  /// - [lineHeight] 每一行的高度
  ///
  /// 返回：
  /// - 若需要滚动，则返回新的 `scrollY` 值；
  /// - 若光标已在视口范围内，则返回 `null`。
  double? computeScrollToReveal({
    required int cursorPosition,
    required List<String> lines,
    required double viewportHeight,
    required double currentScrollY,
    required double padding,
    required double lineHeight,
  }) {
    if (viewportHeight <= 0 || lines.isEmpty) return null;

    // 计算 cursor 所在行号（row）
    int remain = cursorPosition;
    int row = 0;
    for (final line in lines) {
      final int lenWithNL = line.length + 1; // +1 for '\n'
      if (remain <= line.length) break;
      remain -= lenWithNL;
      row++;
      if (row >= lines.length) {
        row = lines.length - 1;
        break;
      }
    }

    // 行的上下边界坐标
    final double yTop = padding + row * lineHeight;
    final double yBottom = yTop + lineHeight;

    // 内容总高度 & 最大滚动范围
    final double contentHeight = padding * 2 + lines.length * lineHeight;
    final double maxScroll =
    contentHeight > viewportHeight ? (contentHeight - viewportHeight) : 0.0;

    double newScroll = currentScrollY;

    // 判断是否需要滚动
    if (yTop < currentScrollY) {
      // 光标在视口上方 → 向上滚
      newScroll = yTop - padding;
    } else if (yBottom > currentScrollY + viewportHeight) {
      // 光标在视口下方 → 向下滚
      newScroll = yBottom - viewportHeight;
    }

    // 限制在合法范围
    if (newScroll < 0.0) newScroll = 0.0;
    if (newScroll > maxScroll) newScroll = maxScroll;

    // 如果滚动值有变化，返回新值；否则返回 null
    if (newScroll != currentScrollY) {
      return newScroll;
    }
    return null;
  }
}
